package cn.wzl.recognition.bpnn;

import cn.wzl.recognition.bpnn.util.PictureRecognitionConverter;
import cn.wzl.recognition.bpnn.util.RandomUtil;

import java.util.Random;

public class PictureRecognitionBPNN {

    private final double[][] layerOutputs;
    private final double[][] dLayerOutputs;

    private final double[][][] layersWeight;
    private final double[][][] lastDLayersWeight;

    private final double eta;
    private final double forgetRate;
    private final double momentum;

    private final boolean isDebugMode;

    public PictureRecognitionBPNN(int[] networkStructure, double eta, double forgetRate, double momentum, boolean isDebugMode) {
        this.isDebugMode = isDebugMode;
        int randomSeed = 1017320211;//new Random().nextInt();
        System.out.println("random seed is:" + randomSeed);
        //-512425216 //1017320211
        RandomUtil.setRandomSeed(randomSeed);

        layerOutputs = buildDoubleNodeArrayWithNetworkStructure(networkStructure);

        int[] dLayerNetworkStructure = new int[networkStructure.length - 1];
        System.arraycopy(networkStructure, 1, dLayerNetworkStructure, 0, dLayerNetworkStructure.length);
        dLayerOutputs = buildDoubleNodeArrayWithNetworkStructure(dLayerNetworkStructure);

        layersWeight = buildDoubleRelationArrayWithNetworkStructure(networkStructure);
        lastDLayersWeight = buildDoubleRelationArrayWithNetworkStructure(networkStructure);

        for (int i = 0; i < layersWeight.length; i++) {
            for (int j = 0; j < layersWeight[i].length; j++) {
                for (int k = 0; k < layersWeight[i][j].length; k++) {
                    double real = RandomUtil.randomDouble();
                    layersWeight[i][j][k] = RandomUtil.randomDouble() > 0.5 ? real : -real;
                }
            }
        }

        this.eta = eta;
        this.forgetRate = forgetRate;
        this.momentum = momentum;
    }

    private static double[][] buildDoubleNodeArrayWithNetworkStructure(int[] networkStructure) {
        double[][] ret = new double[networkStructure.length][];
        for (int i = 0; i < networkStructure.length; i++) {
            ret[i] = new double[networkStructure[i] + 1];
        }
        return ret;
    }

    private static double[][][] buildDoubleRelationArrayWithNetworkStructure(int[] networkStructure) {
        double[][][] ret = new double[networkStructure.length - 1][][];
        for (int i = 0; i < networkStructure.length - 1; i++) {
            ret[i] = new double[networkStructure[i] + 1][networkStructure[i + 1] + 1];
        }
        return ret;
    }


    public int training(double[] input, double[] expectedValue) {
        double[] calculateExpectedValue = new double[expectedValue.length + 1];
        System.arraycopy(expectedValue, 0, calculateExpectedValue, 1, expectedValue.length);
        System.arraycopy(input, 0, layerOutputs[0], 1, input.length);

        double[] actualValue = forwardPropagation();
        backPropagation(calculateExpectedValue, actualValue);
        return PictureRecognitionConverter.convertOutputArrayToNumber(actualValue, false);
    }

    public int run(double[] input) {
        System.arraycopy(input, 0, layerOutputs[0], 1, input.length);
        return PictureRecognitionConverter.convertOutputArrayToNumber(forwardPropagation(), isDebugMode);
    }


    private double[] forwardPropagation() {
        for (int i = 0; i < layerOutputs.length - 1; i++) {
            forwardToNextLayer(layerOutputs[i], layerOutputs[i + 1], layersWeight[i]);
        }
        return layerOutputs[layerOutputs.length - 1];
    }

    private void forwardToNextLayer(double[] layer, double[] nextLayer, double[][] w) {
        layer[0] = 1;
        for (int i = 1; i < nextLayer.length; i++) {
            double sum = 0;
            for (int j = 0; j < layer.length; j++) {
                sum += layer[j] * w[j][i];
            }
            nextLayer[i] = active(sum);
        }
    }

    private void backPropagation(double[] expectedValue, double[] actualValue) {
        for (int i = 1; i < expectedValue.length; i++) {
            dLayerOutputs[dLayerOutputs.length - 1][i] = dActive(actualValue[i]) * loss(expectedValue[i], actualValue[i]);
        }

        for (int i = layerOutputs.length - 2; i > 0; i--) {
            calculateDPreLayer(i);
        }

        for (int i = layerOutputs.length - 1; i > 0; i--) {
            layerOutputs[i - 1][0] = 1;
            for (int j = 1; j < dLayerOutputs[i - 1].length; j++) {  //w[i+1]
                for (int k = 0; k < layerOutputs[i - 1].length; k++) {  //w[i]
                    //dw = d(w*x)/dw * dy/d(w*x)  = x*dLayerOutput = layerOutput[i] * dLayerOutput[i+1]
                    double dw = momentum * lastDLayersWeight[i - 1][k][j] + eta * dLayerOutputs[i - 1][j] * layerOutputs[i - 1][k];
                    layersWeight[i - 1][k][j] = (1 - forgetRate) * layersWeight[i - 1][k][j] + dw;
                    lastDLayersWeight[i - 1][k][j] = dw;
                }
            }
        }

    }

    private void calculateDPreLayer(int layerIndex) {
        for (int i = 1; i < dLayerOutputs[layerIndex - 1].length; i++) {
            double sum = 0;
            for (int j = 1; j < dLayerOutputs[layerIndex].length; j++) {
                sum += layersWeight[layerIndex][i][j] * dLayerOutputs[layerIndex][j];
            }
            dLayerOutputs[layerIndex - 1][i] = sum * dActive(layerOutputs[layerIndex][i]);
        }
    }

    private void loadValue(double[] input, double[] layer) {
        System.arraycopy(input, 0, layer, 1, input.length);
    }

    //d(sigmoid(x)) = sigmoid(x) * (1 - sigmoid(x))
    private double dActive(double fActive) {
        return fActive * (1d - fActive);
    }

    private double active(double value) {
        return sigmoid(value);
    }

    private double loss(double expectedValue, double actualValue) {
        return expectedValue - actualValue;
    }

    private double sigmoid(double value) {
        return 1d / (1d + Math.exp(-value));
    }

}
